package com.njitzx.aop;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.alipay.api.domain.JointAccountBillDetail;
import com.njitzx.annotation.Log;
import com.njitzx.en.BusinessStatus;
import com.njitzx.entity.po.OperLog;
import com.njitzx.entity.po.User;
import com.njitzx.entity.vo.UserQueryDTO;
import com.njitzx.service.OperLogService;
import com.njitzx.service.UserService;
import com.njitzx.util.BaseContext;
import com.njitzx.util.IpUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.AfterThrowing;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

/**
 * 日志切面
 */
@Component  //交给spring管理
@Aspect  //添加切面
@RequiredArgsConstructor
public class LogAspect {
    private static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
    private final OperLogService operLogService;
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<>("Cost Time");//存储时间

    private final UserService userService;

    /**
     * 返回通知
     */

    @Before(value = "@annotation(log)")
    public void boBefore(JoinPoint joinPoint, Log log) {
        TIME_THREADLOCAL.set(System.currentTimeMillis()); //存储当前的时间
    }

    //方法执行之后

    @AfterReturning(returning = "jsonResult", pointcut = "@annotation(log)")
    public void doAfterReturning(JoinPoint joinPoint, Log log, Object jsonResult) {
        //调用处理日志的方法
        handleLog(joinPoint, log, null, jsonResult);
    }
    @AfterThrowing(pointcut = "@annotation(log)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log log, Exception e) {
        handleLog(joinPoint, log, e, null);
    }
    private void handleLog(JoinPoint joinPoint, Log log, Exception e, Object jsonResult) {
        System.out.println("执行逻辑处理的日志记录");
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return;
        }
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        HttpServletRequest request =servletRequestAttributes.getRequest();


        OperLog operLog = new OperLog(); //创建日志对象
        if (e != null) {
            operLog.setStatus(BusinessStatus.FAIL.ordinal()); //枚举类中的属性
            operLog.setErrorMsg(e.getMessage());
        }
        String ipAddress = IpUtil.getIpAddress(request); //拿到ip地址
        operLog.setOperIp(ipAddress);
        operLog.setOperLocation("局域网"); //这边可以调用地图,实现位置查找
        operLog.setOperUrl(request.getRequestURL().toString());
        operLog.setOperTime(LocalDateTime.now());
        //拿到当前操作的id
        Integer userID = BaseContext.getCurrentId();
        UserQueryDTO user = userService.findById(userID);
        //拿到当前的用户  拿到当前的ip
        String username = user.getUsername();
        String rolename = user.getRoleName();

        operLog.setOperName(username);
        operLog.setRoleName(rolename);

        //设置请求方法
        operLog.setRequestMethod(request.getMethod()); //拿到reqeust方法

        //获取目标对象的类名
        //从切入点（joinPoint）中获取目标对象的类名和方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        operLog.setMethod(className + "." + methodName + "()");

        //获取注解中方法的描述星
        getControllerMethodDescription(joinPoint, log, jsonResult, operLog);

        long executeTime = System.currentTimeMillis() - TIME_THREADLOCAL.get();
        operLog.setCostTime(executeTime);

        operLogService.add(operLog);
    }

    private void getControllerMethodDescription(JoinPoint joinPoint, Log log, Object jsonResult, OperLog operLog) {
        operLog.setTitle(log.title()); //设置操作模块
        operLog.setBusinessType(log.businessType().ordinal());

        if (log.isSaveRequestData()) {
            //设置请求参数
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }

        if (log.isSaveRequestData() && !StringUtils.isEmpty(jsonResult)) {
            //设置响应结果
            operLog.setJsonResult(JSON.toJSONString(jsonResult));
        }
    }

    /**
     * 获取当前HTTP请求的参数
     *
     * @return 一个Map，映射参数名称到参数值数组。这允许处理多值参数。
     */
    private static Map<String, String[]> getParameterMap() {
        // 从Spring的RequestContextHolder中获取当前请求的属性
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 将RequestAttributes强制转换为ServletRequestAttributes，以便访问HTTP请求特定的属性
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        // 从ServletRequestAttributes中获取当前HTTP请求对象
        HttpServletRequest request = (HttpServletRequest) servletRequestAttributes.getRequest();
        // 获取请求的所有参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        return parameterMap;
    }

    /**
     * 设置操作日志的请求参数信息。
     *
     * @param joinPoint         切点，用于获取方法参数。
     * @param operLog           操作日志对象，用于设置请求参数信息。
     * @param excludeParamNames 需要排除的参数名数组，这些参数不会被记录在日志中。
     */
    private void setRequestValue(JoinPoint joinPoint, OperLog operLog, String[] excludeParamNames) {
        // 获取当前请求的属性
        Map<String, String[]> parameterMap = getParameterMap();
        // 如果参数不为空且不为空集合
        if (parameterMap != null && !parameterMap.isEmpty()) {
            // 将参数转换为JSON字符串，通过excludePropertyPreFilter过滤掉不需要记录的参数
            String params = JSONObject.toJSONString(parameterMap, excludePropertyPreFilter(excludeParamNames));
            // 设置操作日志的请求参数，截取前2000个字符以防止过长
            operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params, 0, 2000));
        } else {
            // 如果请求参数为空，尝试从方法参数中获取信息
            Object args = joinPoint.getArgs();
            // 如果方法参数不为空
            if (args != null) {
                // 将方法参数转换为字符串，同样支持排除某些参数名
                String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
                // 设置操作日志的请求参数，同样截取前2000个字符
                operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params, 0, 2000));
            }
        }
    }

    /**
     * 忽略敏感属性
     *
     * @param excludeParamNames 需要排除的参数名数组
     * @return {@link PropertyPreFilters.MySimplePropertyPreFilter }
     */
    public PropertyPreFilters.MySimplePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) {
        return new PropertyPreFilters().addFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }

    /**
     * 将对象数组转换为字符串，排除指定的参数名（敏感参数）。
     *
     * @param paramsArray       参数数组，可以包含任意类型的对象。
     * @param excludeParamNames 需要排除的参数名数组，这些参数不会被转换为字符串。
     * @return 返回转换后的参数字符串，各参数间以空格分隔。
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        // 使用StringBuilder来构建最终的参数字符串
        StringBuilder params = new StringBuilder();
        // 检查参数数组是否为空或长度为0，避免不必要的处理
        if (paramsArray != null) {
            // 遍历参数数组中的每个对象
            for (Object o : paramsArray) {
                // 检查对象是否为空且不属于被过滤的类型
                if (o != null && !isFilterObject(o)) {
                    try {
                        // 将对象转换为JSON字符串，排除指定的属性
                        Object jsonObj = JSONObject.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
                        // 将转换后的JSON字符串追加到参数字符串中，并以空格分隔各个参数
                        params.append(jsonObj).append(" ");
                    } catch (Exception ignored) {
                        // 忽略转换过程中的异常，确保方法的健壮性
                    }
                }
            }
        }
        return params.toString().trim();
    }

    /**
     * 判断传入的对象是否需要被过滤。
     * 这个方法主要用于处理上传文件时，判断接收的参数是否为文件类型或其他特定类型。
     *
     * @param o 待检查的对象
     * @return 如果对象需要被过滤（即对象为MultipartFile或其他特定类型），则返回true；否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        // 获取对象的类类型
        Class<?> clazz = o.getClass();

        // 检查对象是否为数组类型
        if (clazz.isArray()) {
            // 如果数组的组件类型可以被MultipartFile类转换，则返回true
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            // 如果对象是集合类型，将其转换为Collection接口实例
            Collection collection = (Collection) o;
            // 遍历集合中的每个元素，如果任意元素是MultipartFile实例，则返回true
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            // 如果对象是Map类型，将其转换为Map接口实例
            Map map = (Map) o;
            // 遍历Map中的每个条目，如果任意条目的值是MultipartFile实例，则返回true
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        // 如果对象不是数组、集合或Map类型，检查它是否为MultipartFile、HttpServletRequest、HttpServletResponse或BindingResult实例
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}
